/* Copyright © 2023 - 2024 Coremail论客
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

import { getLogger } from "./log";
import {
  decodeMimeString,
  qpEncode,
  base64Encode,
  encodeMimeString,
  getEncodeMethod,
  decodeCharset,
  base64DecodeStream,
  qpDecodeStream,
  base64EncodeStream,
} from "./encodings";
import { CMError, ErrorCode,
  MailAttachment,
  MailItem, MailPart,
  MimeDispositionType,
  TransferEncoding } from "../api";
import type {
  EmailAddress,
  MailEnvelope,
  InlineImage,
  Attachment,
  MailStructure,
  MimeDisposition,
  MimeParams,
  IBuffer,
  ContentType,
  Mime,
  IBufferCreator,
  MimeHeader,
} from "../api";
import { CRLF, parseDateString, sCRLF } from "./common";
import { memBufferCreator, string2buffer } from "./file_stream";
import { IReader, StringReader, BufferReader } from "./data_reader"
import {
  capitalize, encodeAddress, encodeAddressList, generateBoundary,
  parseAddress, parseContentType, parseDisposition, stringifyContentType,
  MimeHeaderName as MHN, stringifyDisposition,
  generateMessageId,
  MimeHeaderImpl} from './mime_helper';

const logger = getLogger("mime");

export type MIMEHeadEncode = Map<string, { value: string; encoder?: "Q" | "B" }>;

export type EncodeOption = {
  qpThreshold?: number;
  isRichText?: boolean;
  needEncode?: boolean;
  boundaryIndex?: number;
};

/*
+--------------------- multipart/mixed ---------------------------------+
| +--------- multipart/related ----------------------+  +------------+ |
| | +------ multipart/alternative --+  +-----------+  |  | image/jpeg | |
| | | +------------+ +-----------+  |  | image/png |  |  | image/png  | |
| | | | text/plain | | text/html |  |  | image/gif |  |  | image/gif  | |
| | | +------------+ +-----------+  |  | other/mime|  |  | other/mime | |
| | +-------------------------------+  +-----------+  |  +------------+ |
| +---------------------------------------------------+                 |
+-----------------------------------+-----------------+-----------------+
*/


export async function serializeMail(
  mailHead: Omit<MailEnvelope, "date" | "messageId">,
  extra: MIMEHeadEncode,
  body: string,
  plainBody: string,
  innerImages: InlineImage[],
  attachments: Attachment[],
  bufferCreator: IBufferCreator = memBufferCreator
): Promise<IBuffer> {
  const extraHeader: MimeHeader = new MimeHeaderImpl();
  for (const [k, v] of extra.entries()) {
    extraHeader.set(k, v.value);
  }
  const attachmentList: MailAttachment[] = attachments.map(a => {
    const contentType = parseContentType(a.contentType)
    contentType.params.name = a.name;
    return {
      contentType,
      disposition: {
        type: 'attachment',
        params: {
          filename: a.name
        }
      },
      getFileName: () => a.name,
      getBody: () => Promise.resolve(a.body),
      partId: '',
      size: 0,
    };
  })
  const inlineList: MailAttachment[] = innerImages.map(a => {
    const contentType = parseContentType(a.contentType)
    contentType.params.name = a.name;
    return {
      contentType,
      disposition: {
        type: 'inline',
        params: {
          filename: a.name
        }
      },
      contentId: a.contentId,
      getFileName: () => a.name,
      getBody: () => Promise.resolve(a.body),
      partId: '',
      size: 0,
    };
  })
  const mail: MailItem = {
    ...mailHead,
    date: new Date,
    messageId: '',
    html: body,
    plain: plainBody,
    replyTo: [],
    extraHeader,
    attachments: attachmentList,
    inlines: inlineList,
  };
  const res = await stringifyMail(mail, bufferCreator);
  return res;
  // return res.readAll();
}

export async function serializeMIME(
  headers: Array<[string, string]>,
  body: string,
  option: EncodeOption = {}
): Promise<string> {
  const mime: Mime = {
    headers: new MimeHeaderImpl(headers),
    body: string2buffer(body),
    contentType: parseContentType('text/plain'),
    children: [],
    partId: '',
    size: 0
  }
  const res = await stringifyMime(mime);
  return res.readAll();
}

export async function createTextMime(
  text: string,
  subType: 'html' | 'plain',
  encoding: TransferEncoding
): Promise<Mime> {
  const body = memBufferCreator.createBuffer();
  if (encoding == 'quoted-printable') {
    const content = qpEncode(text, false);
    await body.end(content);
  } else {
    encoding = 'base64'
    const content = base64Encode(text);
    await body.end(content);
  }
  // body.end(text);
  const mime: Mime = {
    contentType: {
      type: 'text',
      subType,
      params: {
        charset: 'utf-8'
      }
    },
    encoding,
    headers: new MimeHeaderImpl(),
    body: body,
    children: [],
    partId: '',
    size: text.length
  }
  mime.headers.set(MHN.ContentType, stringifyContentType(mime.contentType));
  mime.headers.set(MHN.ContentTransferEncoding, mime.encoding);
  return mime;
}

export async function createBinaryMime(
  type: string,
  subType: string,
  disposition: MimeDispositionType,
  fileName: string,
  content: IBuffer,
  contentId?: string,
  bufferCreator?: IBufferCreator
): Promise<Mime> {
  const body = await base64EncodeStream(content, bufferCreator || memBufferCreator);
  const mime: Mime = {
    contentType: {
      type,
      subType,
      params: {
        name: fileName
      }
    },
    disposition: {
      type: disposition,
      params: {
        filename: fileName
      }
    },
    contentId,
    encoding: 'base64',
    headers: new MimeHeaderImpl(),
    body,
    children: [],
    partId: '',
    size: 0
  }
  if (mime.contentId) {
    mime.headers.replace(MHN.ContentID, mime.contentId);
  }
  mime.headers.replace(MHN.ContentType, stringifyContentType(mime.contentType));
  if (mime.disposition) {
    mime.headers.replace(MHN.ContentDisposition, stringifyDisposition(mime.disposition));
  }
  mime.headers.replace(MHN.ContentTransferEncoding, mime.encoding);
  return mime;
}

export async function createMultipartMime(
  mimeList: Mime[],
  subType: string,
  level: number
): Promise<Mime> {
  const body = memBufferCreator.createBuffer();
  await body.end();
  const mime: Mime = {
    contentType: {
      type: 'multipart',
      subType,
      params: {
        boundary: generateBoundary(level),
      }
    },
    headers: new MimeHeaderImpl(),
    body: body,
    children: mimeList,
    partId: '',
    size: mimeList.reduce((p, c) => p + c.size, 0)
  }
  mime.headers.replace(MHN.ContentType, stringifyContentType(mime.contentType));
  return Promise.resolve(mime);
}

async function attachment2mime(attach: MailAttachment[], disposition: MimeDispositionType): Promise<Mime[]> {
  const attachmentMimeList = await Promise.all(attach.map(
    async attachment => {
      const fileName = attachment.getFileName();
      const ct = attachment.contentType;
      return createBinaryMime(
        ct.type, ct.subType, disposition, fileName,
        await attachment.getBody(), attachment.contentId);
    }));
  return attachmentMimeList;
}

export async function stringifyMail(mail: MailItem, bufferCreator?: IBufferCreator): Promise<IBuffer> {
  let htmlMime: Mime | undefined;
  let plainMime: Mime | undefined;
  let alternativeMime: Mime | undefined;
  let relatedMime: Mime | undefined;
  let mixedMime: Mime | undefined;
  let mime: Mime | undefined;
  let level = 1;
  if (mail.html.length > 0) {
    htmlMime = await createTextMime(mail.html, 'html', 'quoted-printable');
  }
  if (mail.plain.length > 0) {
    const method = getEncodeMethod(mail.plain);
    plainMime = await createTextMime(
      mail.plain, 'plain', method == 'Q' ? 'quoted-printable' : 'base64');
  } else if (!mail.html) {
    plainMime = await createTextMime('', 'plain', 'base64');
  }
  if (mail.attachments.length > 0) {
    const attachmentMimeList = await attachment2mime(mail.attachments, 'attachment');
    mixedMime = await createMultipartMime(attachmentMimeList, 'mixed', level);
    level += 1;
    mime = mixedMime;
  }
  if (mail.inlines.length > 0) {
    const inlineMimeList = await attachment2mime(mail.inlines, 'inline');
    relatedMime = await createMultipartMime(inlineMimeList, 'related', level);
    level += 1;
    if (mixedMime) {
      mixedMime.children.unshift(relatedMime);
    } else {
      mime = relatedMime;
    }
  }
  if (htmlMime && plainMime) {
    alternativeMime = await createMultipartMime([htmlMime, plainMime], 'alternative', level);
    if (relatedMime) {
      relatedMime.children.unshift(alternativeMime);
    } else if (mixedMime) {
      mixedMime.children.unshift(alternativeMime);
    } else {
      mime = alternativeMime;
    }
  } else {
    const m = htmlMime || plainMime;
    if (relatedMime) {
      relatedMime.children.unshift(m);
    } else if (mixedMime) {
      mixedMime.children.unshift(m);
    } else {
      mime = m;
    }
  }
  if (!mime) { // 一般不可能出现
    logger.error('compose mime failed')
    return Promise.reject(new CMError('error', ErrorCode.Error));
  }
  const h = mime.headers;
  h.replace(MHN.From, encodeAddress(mail.from));
  if (mail.to.length) {
    h.replace(MHN.To, encodeAddressList(mail.to));
  }
  if (mail.cc.length) {
    h.replace(MHN.CC, encodeAddressList(mail.cc));
  }
  if (mail.bcc.length) {
    h.replace(MHN.BCC, encodeAddressList(mail.bcc));
  }
  h.replace(MHN.Subject, encodeMimeString(mail.subject));
  h.replace(MHN.Date, mail.date.toUTCString());
  h.replace(MHN.MessageID, mail.messageId || generateMessageId());
  h.replace(MHN.ContentType, stringifyContentType(mime.contentType));
  if (mail.priority) {
    h.replace(MHN.XPriority, mail.priority.toString(10));
  }
  for (const [k, v] of mail.extraHeader.entries()) {
    h.replace(k, v);
  }
  return stringifyMime(mime, bufferCreator);
}

export async function stringifyMime(mime: Mime, bufferCreator?: IBufferCreator): Promise<IBuffer> {
  const res = bufferCreator ? bufferCreator.createBuffer({}) : memBufferCreator.createBuffer({});
  await _stringifyMime(mime, 1, res);
  await res.end();
  return res;
}

async function _stringifyMime(mime: Mime, level: number, out: IBuffer): Promise<void> {
  for (let [field, value] of mime.headers.entries()) {
    await out.feed(`${capitalize(field)}: ${value}\r\n`);
  }
  await out.feed(CRLF);
  await out.append(mime.body);
  if (mime.contentType.type == 'multipart' && mime.children.length > 0) {
    const boundary = mime.contentType.params.boundary;
    for (const m of mime.children) {
      await out.feed(`--${boundary}\r\n`)
      await _stringifyMime(m, level + 1, out);
    }
    await out.feed(`--${boundary}--`)
  }
  await out.feed(CRLF);
}

export async function parseMime(s: string | IBuffer, bufferCreator?: IBufferCreator): Promise<Mime> {
  logger.trace('start parse mime')
  if (!bufferCreator) {
    bufferCreator = memBufferCreator;
  }
  const reader = typeof s == 'string' ?
    new StringReader(s) :
    new BufferReader(s.readRaw());
  const mime = await _parseMime(reader, '', bufferCreator);
  modifyMimePartId(mime);
  return mime;
}

export async function parseMimeHeader(reader: IReader): Promise<MimeHeader> {
  const headers: MimeHeader = new MimeHeaderImpl();
  await reader.skipSpace()
  while (!(await reader.end())) { // header
    const field = await reader.readUntil(':');
    const valueParts = [await reader.readUntil(sCRLF)];
    let nextChar = await reader.peek(2);
    while (nextChar[0] == ' ' || nextChar[0] == '\t') {
      await reader.skip(1);
      const v = await reader.readUntil(sCRLF);
      valueParts.push(v);
      nextChar = await reader.peek(2);
    }
    const value = valueParts.join('').trim();
    headers.set(field, value);
    if (nextChar == sCRLF) {
      await reader.skip(2);
      break;
    }
  }
  return headers;
}

export async function _parseMime(
  reader: IReader,
  boundary: string,
  bufferCreator: IBufferCreator
): Promise<Mime> {
  logger.trace('start parse')
  const mime: Mime = {
    contentType: { type: "", subType: "", params: {} },
    headers: new MimeHeaderImpl(),
    children: [],
    body: bufferCreator.createBuffer({useFile: false}),
    size: 0,
    partId: "",
  };
  const CRLF = "\r\n";
  await reader.skipSpace();
  while (!(await reader.end())) {
    const headers = await parseMimeHeader(reader);
    mime.headers = headers;
    for (const [field, value] of headers.entries()) {
      processHeadValue(mime, field, value);
    }
    logger.trace('parse body', mime.contentType.type)
    if (mime.contentType.type == 'multipart') {
      await reader.skipSpace();
      const selfBoundary = mime.contentType.params.boundary;
      if (!selfBoundary) {
        logger.error("no boundary");
      }
      const content = await reader.readUntil('--' + selfBoundary);
      if (content.length > 0) {
        await mime.body.end(content);
      }

      while (!(await reader.end())) {
        let chars = await reader.peek(2);
        if (chars == '--') {
          reader.skip(2);
          if (boundary) {
            await reader.readUntil('--' + boundary);
          }
          return mime;
        } else if (chars == CRLF) {
          reader.skip(2);
          const m = await _parseMime(reader, selfBoundary, bufferCreator);
          m.size = await m.body.getSizeRaw();
          mime.children.push(m);
        } else {
          logger.error('error');
          throw new CMError('parse error');
        }
      }
    } else {
      const content = boundary ?
        await reader.readBlockUntil('--' + boundary) :
        await reader.readToEnd();
      mime.body = content;
      break;
    }
    await reader.skipSpace();
  }
  logger.trace('end parse')
  return mime;
}

function processHeadValue(mime: Mime, field: string, value: string) {
  switch (field) {
    case MHN.ContentType.toLowerCase():
      mime.contentType = parseContentType(value);
      break;
    case MHN.ContentDisposition.toLowerCase():
      mime.disposition = parseDisposition(value);
      break;
    case MHN.ContentID.toLowerCase():
      mime.contentId = value;
      break;
    case MHN.ContentTransferEncoding.toLowerCase():
      mime.encoding = value.toLowerCase() as TransferEncoding;
      break;
  }
}

export const parseMime2 = parseMime;

export function isHtmlPart(m: MailPart) {
  return m.contentType.type == "text" &&
    m.contentType.subType == 'html';
}

export function isPlainPart(m: MailPart) {
  return m.contentType.type == "text" &&
    m.contentType.subType == 'plain';
}

export function isInlineImagePart(m: MailPart) {
  return (m.disposition?.type == "inline") ||
    (m.contentType.type == 'image' && !!m.contentId);
}

export function isAttachmentPart(m: MailPart) {
  return (m.disposition?.type == "attachment") || (!isInlineImagePart(m) &&
    m.contentType.type !== 'multipart' &&
    !isHtmlPart(m) &&
    !isPlainPart(m));
}

export class MimeMail {
  _mime: Mime;
  constructor(mime: Mime) {
    this._mime = mime;
  }

  getPlainPart(): MailStructure | undefined {
    return getMimeParts(this._mime, isPlainPart)[0];
  }

  getPlain(): IBuffer | undefined {
    const part = this.getPlainPart();
    if (!part) {
      return;
    }
    return (part as Mime).body;
  }

  getPlainPartId(): string|undefined {
    return this.getPlainPart()?.partId;
  }

  getHtmlPart(): MailStructure | undefined {
    return getMimeParts(this._mime, isHtmlPart)[0];
  }

  getHtmlPartId(): string | undefined {
    return this.getHtmlPart()?.partId;
  }

  getHtml(): IBuffer | undefined {
    const part = this.getHtmlPart();
    if (!part) {
      return;
    }
    return (part as Mime).body;
  }

  getEnvelope(): MailEnvelope {
    return getEnvelopeFromMailHeader(this._mime.headers);
  }

  getStructure(): MailStructure {
    return getStructure2(this._mime);
  }

  getParts(): MailPart[] {
    return getMimeParts(this._mime, mime => true).map(s => ({
      ...s, children: undefined
    }));
  }

  getPartContent(partId: string): IBuffer | undefined {
    const part = getMimeParts(this._mime, mime => mime.partId == partId)[0] as Mime;
    if (part) {
      return part.body;
    }
  }

  getAttachments(): MailStructure[] {
    return getMimeParts(this._mime, isAttachmentPart);
  }

  getInlineImages(): MailStructure[] {
    return getMimeParts(this._mime, isInlineImagePart);
  }
}

export async function decodeData(
  buffer: IBuffer,
  encoding: TransferEncoding,
  bufferCreator: IBufferCreator = memBufferCreator
): Promise<IBuffer> {
  if (encoding == 'base64') {
    return base64DecodeStream(buffer, bufferCreator);
  } else if (encoding == 'quoted-printable') {
    return qpDecodeStream(buffer, bufferCreator);
  } else {
    return buffer;
  }
}

export function getMimePartName(mime: MailPart): string {
  const name = mime.disposition?.params.filename || mime.contentType.params.name;
  return name || mime.partId;
}

export function getMimeParts(mime: MailStructure, filter: (mime: MailStructure) => boolean): MailStructure[] {
  let parts: MailStructure[] = [];
  if (filter(mime)) {
    parts.push(mime);
  }
  if (mime.children.length > 0) {
    parts = mime.children.reduce(
      (prev, m) => prev.concat(getMimeParts(m, filter)),
      parts
    );
  }
  return parts;
}

export function getEnvelopeFromMailHeader(headers: MimeHeader): MailEnvelope {
  let dateStr = headers.get(MHN.Date);
  if (!dateStr) {
    const r = headers.get(MHN.Received);
    if (r) {
      dateStr = r.split(';').pop();
    }
  }
  return {
    from: parseAddress(decodeMimeString(headers.get(MHN.From) || ""))[0],
    to: parseAddress(decodeMimeString(headers.get(MHN.To) || "")),
    cc: parseAddress(decodeMimeString(headers.get(MHN.CC) || "")),
    bcc: parseAddress(decodeMimeString(headers.get(MHN.BCC) || "")),
    subject: decodeMimeString(headers.get(MHN.Subject) || ""),
    date: parseDateString(dateStr || ""),
    messageId: headers.get(MHN.MessageID),
  };
}

function getStructure2(mime: Mime): MailStructure {
  return {
    contentType: mime.contentType,
    disposition: mime.disposition,
    contentId: mime.contentId,
    encoding: mime.encoding,
    children: mime.children.map(getStructure2),
    partId: mime.partId,
    size: mime.size,
  }
}

function modifyMimePartId(mime: Mime): void {
  _modifyMimePartId(mime, []);
}

function _modifyMimePartId(mime: Mime, partIds: number[]): void {
  if (mime.contentType.type == "multipart") {
    mime.children.forEach((m, i) => {
      _modifyMimePartId(m, [...partIds, i + 1]);
    });
  } else {
    mime.partId = partIds.join(".") || "1";
  }
}
